<?php

namespace slinks\core\dependencyInjectionExt\annotation;

use slinks\core\dependencyInjectionExt\Reference;
use slinks\common\addendum\Addendum;
use slinks\common\addendum\ReflectionAnnotatedClass;
use slinks\core\DependencyInjection\Definition;
use slinks\core\DependencyInjection\ContainerBuilder;

/**
 * Utility to define service definitions based upon annotations. 
 * 
 * @author Navid Mitchell
 *
 */
class ServiceDefiningAnnotationUtil {

    private static $init = false;

    private function __construct() {

    }

    // FIXME: this must be used by the ActionAnnotaionVisitor so it recoginizes the other class type annotations.
    public static function tryInit() {
        if (!self::$init) {
            // these are included here so adendum will pick them up with thier short name when used as doc_block annotations.
            include_once 'Argument.php';
            include_once 'Inject.php';
            include_once 'Configurator.php';
            include_once 'PostConstruct.php';
            include_once 'FactoryMethod.php';
            include_once 'Scope.php';

            // register the shortcut Annotation names with the Addendum
            Addendum::registerClass('Scope', '\\slinks\\core\\dependencyInjectionExt\\annotation\\Scope');
            Addendum::registerClass('PostConstruct', '\\slinks\\core\\dependencyInjectionExt\\annotation\\PostConstruct');
            Addendum::registerClass('FactoryMethod', '\\slinks\\core\\dependencyInjectionExt\\annotation\\FactoryMethod');
            Addendum::registerClass('Configurator', '\\slinks\\core\\dependencyInjectionExt\\annotation\\Configurator');
            Addendum::registerClass('Argument', '\\slinks\\core\\dependencyInjectionExt\\annotation\\Argument');
            Addendum::registerClass('Inject', '\\slinks\\core\\dependencyInjectionExt\\annotation\\Inject');
            Addendum::registerClass('Qualifier', '\\slinks\\core\\dependencyInjectionExt\\annotation\\Qualifier');
        }
    }

    /**
     * Creates a service Definition for the given ReflectionAnnotatedClass.
     * This method uses the classes annotations to determine how the service should be defined.
     * @param ReflectionAnnotatedClass $classAnnotationInfo the ReflectionAnnotatedClass for the class containing the Annotations.
     * 		  This is part of the Addendum project http://code.google.com/p/addendum/
     */
    public static function createDefinition(ReflectionAnnotatedClass $classAnnotationInfo) {

        self::tryInit();

        $serviceDefinition = new Definition($classAnnotationInfo->getName());

        // get scope
        if ($classAnnotationInfo->hasAnnotation('Scope')) {
            $scope = $classAnnotationInfo->getAnnotation('Scope');
            $serviceDefinition->setScope($scope->value);
            $serviceDefinition->setPublic($scope->public);
        }

        // now get any arguments for the service
        if ($classAnnotationInfo->hasAnnotation('Argument')) {
            $serviceDefinition->setArguments(self::getArgumentsAsPhp($classAnnotationInfo->getAllAnnotations('Argument')));
        }

        // now parse any Configurator
        if ($classAnnotationInfo->hasAnnotation('Configurator')) {
            $serviceDefinition->setConfigurator(self::getConfigurator($classAnnotationInfo->getAnnotation('Configurator'), $classAnnotationInfo->getName()));
        }

        if ($classAnnotationInfo->hasAnnotation('Qualifier')) {
            $qualifiers = array();
            foreach ($classAnnotationInfo->getAllAnnotations('Qualifier') as $qualifier) {
                $qualifiers[] = $qualifier->value;
            }
            $serviceDefinition->addTag('qualifier', $qualifiers);
        }

        // now process any method annotations
        self::parseMethodAnnotations($serviceDefinition, $classAnnotationInfo->getMethods());

        // now process any parameter annotations
        //self::parsePropertyAnnotations($serviceDefinition,$classAnnotationInfo->getProperties());

        return $serviceDefinition;
    }

    /**
     * Processes any method annotations and updates the $serviceDefinition by reference.
     * @param Definition $serviceDefinition
     * @param array $propertyAnnotations of ReflectionAnnotatedProperty objects
     */
    private static function parsePropertyAnnotations(Definition &$serviceDefinition, array $propertyAnnotations) {

        /* @var $property ReflectionAnnotatedProperty */
        foreach ($propertyAnnotations as $property) {
            if ($property->hasAnnotation('Inject')) {
//				/* @var $autowired Inject */
//				$autowired = $property->getAnnotation('Inject');
//				$serviceName = null;
//				if(isset($autowired->value)){
//					$serviceName = $autowired->value;
//				}else{
//					// get the service name from the property name 
//					$serviceName = $property->getName();
//				}
//				$serviceDefinition->add
//				PropertySettingService
            }
        }
    }

    /**
     * Processes any method annotations and updates the $serviceDefinition by *reference*.
     * @param sfServiceDefinition $serviceDefinition
     * @param array $methodAnnotations of ReflectionAnnotatedMethod objects
     */
    private static function parseMethodAnnotations(Definition &$serviceDefinition, array $methodAnnotations) {

        /* @var $method ReflectionAnnotatedMethod */
        foreach ($methodAnnotations as $method) {

            // if PostConstruct add a method call with any given arguments
            if ($method->hasAnnotation('PostConstruct')) {
                /* @var $postConstruct PostConstruct */
                $postConstruct = $method->getAnnotation('PostConstruct');
                $serviceDefinition->addMethodCall((string) $method->getName(), isset($postConstruct->value) ? self::getArgumentsAsPhp($postConstruct->value) : array());
            } else if ($method->hasAnnotation('FactoryMethod')) {

                if ($serviceDefinition->getFactoryMethod() != null) {
                    throw new \Exception('FactoryMethod Annotation cannot be used more than once. For ' . $method->getDeclaringClass()->getName());
                }

                if (!$method->isStatic()) {
                    throw new \Exception('FactoryMethod Annotation must be used on static methods only. For ' . $method->getDeclaringClass()->getName());
                }

                /* @var $factoryMethod FactoryMethod */
                $factoryMethod = $method->getAnnotation('FactoryMethod');

                if (isSet($factoryMethod->value)) {
                    // we have arguments passed to the FactoryMethod annotation.
                    // we will use them if they are not already set at the class level
                    if (count($serviceDefinition->getArguments()) > 0) {
                        throw new \Exception('FactoryMethod parameters must not be supplied if the class has Argument annotations. For ' . $method->getDeclaringClass()->getName());
                    }

                    $serviceDefinition->setArguments(self::getArgumentsAsPhp($factoryMethod->value));
                }

                $serviceDefinition->setFactoryMethod((string) $method->getName());
            } else if ($method->hasAnnotation('Inject')) {
                // For any Inject we will create a ServiceReference and pass it to the annotated method.
                // if this method is the constructor we can not have any Argument already specified
                // constructor injection cannot be used with class Argument annotations
                // since the order of the arguments may become obscure.
                if ($method->isConstructor() && count($serviceDefinition->getArguments()) > 0) {
                    throw new \Exception('Inject cannot be used on a constructor if the class has Argument annotations. For ' . $method->getDeclaringClass()->getName());
                }

                /* @var $inject Inject */
                $inject = $method->getAnnotation('Inject');
                $serviceRefs = array();

                if (isset($inject->value)) {
                    if (is_array($inject->value)) {
                        foreach ($inject->value as $value) {
                            $serviceRefs[] = new Reference((string) $value);
                        }
                    } else {
                        $serviceRefs[] = new Reference((string) $inject->value);
                    }
                } else {
                    // try to get the service name from the method parameter type or names
                    $params = $method->getParameters();
                    /* @var $param ReflectionParameter */
                    foreach ($params as $param) {
                        // we use the type hint for the name if provided
                        $class = $param->getClass();
                        $id = '';
                        $interface = false;
                        if ($class != null) {
                            $id = $class->getName();
                            $interface = true;
                            // TODO: add support for qualifier annotation.
                        } else {
                            $id = $param->getName();
                        }

                        $serviceRefs[] = new Reference((string) $id, $interface);
                    }
                }

                if (count($serviceRefs) > 0) {
                    // now see how they need to be used
                    // do they go to the constructor or passed to the service after construction
                    if ($method->isConstructor()) {
                        $serviceDefinition->setArguments($serviceRefs);
                    } else {
                        $serviceDefinition->addMethodCall((string) $method->getName(), $serviceRefs);
                    }
                } else {
                    throw new \Exception('Inject was found but no Service reference could be determined. For ' . $method->getDeclaringClass()->getName() . '.' . $method->getName());
                }
            }
        }
    }

    private static function getConfigurator(Configurator $configurator, string $className) {
        $ret = null;
        if (isset($configurator->function)) {
            $ret = (string) $configurator->function;
        } else {
            if (isset($configurator->service)) {
                $class = new Reference((string) $configurator->service);
            } else if (isset($configurator->class)) {
                $class = (string) $configurator->class;
            } else {
                throw new \Exception('Configurator must have one of (function, class, service) set for ' . $className);
            }
            $ret = array($class, (string) $configurator->method);
        }
        return $ret;
    }

    /**
     * Get PHP arguments for the annotation arguments.
     * @param mixed $argumentAnnotations can be an Array of arguments or a single argument
     * @return the Annotation arguments as an array of PHP argumnets
     */
    private static function getArgumentsAsPhp($argumentAnnotations) {
        $arguments = array();

        // make into an array if not already.
        if (!is_array($argumentAnnotations)) {
            $temp = array();
            $temp[] = $argumentAnnotations;
            $argumentAnnotations = $temp;
        }

        /* @var $arg Argument */
        foreach ($argumentAnnotations as $arg) {

            // the array can contain either actual values or values contained within an @Argument annotation
            $value = null;
            if ($arg instanceof Argument) {
                $value = $arg->value;
            } else {
                $value = $arg;
            }

            // see what the Annotation is
            if ($value instanceof Service) {

                /* @var $service Service */
                $service = $value;
                $arguments[] = new Reference((string) self::getServiceId($service));
            } else if (is_array($value)) {
                // parse array recursively.
                $arguments[] = self::getArgumentsAsPhp($value);
            } else {
                $arguments[] = self::phpize($value);
            }
        }
        return $arguments;
    }

    public function getServiceId(Service $service) {
        $id = null;
        if (isset($service->id)) {
            $id = $service->id;
        } else if (isset($service->value)) {
            $id = $service->value;
        }
        return $id;
    }

    private static function phpize($value) {
        $value = (string) $value;

        switch (true) {
            case 'null' == strtolower($value):
                return null;
            case ctype_digit($value):
                return '0' == $value[0] ? octdec($value) : intval($value);
            case in_array(strtolower($value), array('true', 'on')):
                return true;
            case in_array(strtolower($value), array('false', 'off')):
                return false;
            case is_numeric($value):
                return '0x' == $value[0] . $value[1] ? hexdec($value) : floatval($value);
            case preg_match('/^(-|\+)?[0-9,]+(\.[0-9]+)?$/', $value):
                return floatval(str_replace(',', '', $value));
            default:
                return (string) $value;
        }
    }

}

